/*
 * HttpHelper.cpp
 *
 * Implementation of the HTTP GET, PUT functionalities
 */
#include <arpa/inet.h>
#include <iostream>
#include <netdb.h>
#include <netinet/in.h>
#include <sstream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <errno.h>

#include "HttpHelper.h"
#include "AppLogger.h"
#include "Utils.h"
#include "HttpException.h"

using namespace std;

static AppLogger * logger = AppLogger::getInstance("HttpHelper");

static char _httpReponseBuffer[64 * 1024];

const string CRLF = "\r\n";


/**
 * connects to a server described by the nextHop
 *
 * return the connFD;
 */
int HttpHelper::connectToHop(Hop * nextHop) {
	// do the I/O within the context of a proxySession
	// 1. connect with target and send the request
	char targetPortStr[20];
	sprintf(targetPortStr, "%i", nextHop->port);

	logger->debug("connecting to %s:%s", nextHop->host.c_str(), targetPortStr);

	return HttpHelper::tcp_connect(nextHop->host.c_str(), targetPortStr);

}

/**
 * copied from Pasi's example from network programming class.
 * This function accepts a host name and a "service" and returns
 * a connected socket
 * According to
 * the parameter service, which can be a port number, like "80", or the name of a particular service (found in The IANA Port List or the /etc/services file on your Unix machine) like "http" or "ftp" or "telnet" or "smtp" or whatever.
 */
int HttpHelper::tcp_connect(const char *host, const char *serv)
{
	int sockfd, n;
	struct addrinfo	hints, *res, *ressave;
	char outbuf[80];

	bzero(&hints, sizeof(struct addrinfo));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;

	logger->debug("Resolving %s ...", host);

	if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0) {
		fprintf(stderr, "tcp_connect error for %s, %s: %s\n",
			host, serv, gai_strerror(n));
		return -1;
	}
	ressave = res;

	// loop through all the results and connect to the first we can
	do {
		sockfd = socket(res->ai_family, res->ai_socktype,
				res->ai_protocol);

		if (sockfd < 0) {
			perror("socket");
			logger->debug("Failed to create socket of family %d, type %d",
					res->ai_family, res->ai_socktype);

			continue; /* ignore this one */
		}

		struct sockaddr_in *sin = (struct sockaddr_in *)res->ai_addr;
		const char *ret = inet_ntop(res->ai_family, &(sin->sin_addr),
					    outbuf, sizeof(outbuf));

		if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0) {
			break;		/* success */
		}

		fprintf(stderr, "failed to connect %s, reason: %s\n",ret, strerror(errno));
		close(sockfd);	/* ignore this one */
	} while ( (res = res->ai_next) != NULL);

	if (res == NULL) {	/* errno set from final connect() */
		fprintf(stderr, "tcp_connect error for %s, %s\n", host, serv);
		sockfd = -1;
	} else {
		struct sockaddr_in *sin = (struct sockaddr_in *)res->ai_addr;
		const char *ret = inet_ntop(res->ai_family, &(sin->sin_addr),
					    outbuf, sizeof(outbuf));
//		printf("Address is %s\n", ret);
		logger->debug("Socket connected to %s", ret);
	}

	freeaddrinfo(ressave);

	return(sockfd);
}


HttpHelper::HttpHelper() {
}

HttpHelper::~HttpHelper() {
}

const char * HttpHelper::DOWNLOADED_FILE_NAME = "downloadedFile";

FILE * HttpHelper::DownloadFilefp = NULL;

string HttpHelper::extractPath(string url)
{
	//url is something like:	http://64.236.34.196:80/stream/1020

	string HttpScheme = "http://";
	int pathPartLength = 0;
	size_t startOfScheme = url.find(HttpScheme);

	if (string::npos == startOfScheme) {
		return "";
	}

	// then find the next "/" after "://"
	size_t host_path_seperator_pos = url.find("/",
			startOfScheme + HttpScheme.length());
	if (host_path_seperator_pos != string::npos) {
		pathPartLength = url.length() - (host_path_seperator_pos );
		return url.substr(host_path_seperator_pos, pathPartLength);
	}

	// if no path was specified, the default is the www root
	return "/";

}

//given an HTTP URL, extract the host part
string HttpHelper::extractHost(string url) {

	//url is something like:	http://64.236.34.196:80/stream/1020

	string HttpScheme = "http://";
	int hostPartLength = 0;
	size_t startOfScheme = url.find(HttpScheme);

	if (string::npos == startOfScheme) {
		return "";
	}

	// try to find the ":" which separates host and port
	size_t host_port_seperator_pos = url.find(":",
			startOfScheme + HttpScheme.length());
	if (host_port_seperator_pos != string::npos) {
		// host is the part between endOfScheme and host_port_seperator
		hostPartLength = host_port_seperator_pos
				- (startOfScheme + HttpScheme.length());
		return url.substr(startOfScheme + HttpScheme.length(), hostPartLength);
	}

	//see if there're path after the host and part part. path starts with a "/"
	size_t host_path_seperator_pos = url.find("/",
			startOfScheme + HttpScheme.length());
	if (host_path_seperator_pos != string::npos) {
		hostPartLength = host_path_seperator_pos
				- (startOfScheme + HttpScheme.length());
	} else {
		hostPartLength = url.length() - HttpScheme.length();
	}

	return url.substr(startOfScheme + HttpScheme.length(), hostPartLength);

}

//given an HTTP URL, extract the port
int HttpHelper::extractPort(string url) {

	//url is something like:	http://69.166.45.47:8000/

	string HttpScheme = "http://";
	int portPartLength = 0;
	size_t startOfScheme = url.find(HttpScheme);

	if (string::npos == startOfScheme) {
		//this is not HTTP scheme, return -1;
		return -1;
	}

	// try to find the ":" which separates host and port
	size_t host_port_seperator_pos = url.find(":",
			startOfScheme + HttpScheme.length());
	if (host_port_seperator_pos == string::npos) {
		// if there is no ":" after host, it means the port is default one.
		return 80;
	}

	//see if there're path after the host and part part. path starts with a "/"
	size_t host_path_seperator_pos = url.find("/",
			startOfScheme + HttpScheme.length());
	if (host_path_seperator_pos != string::npos) {
		portPartLength = host_path_seperator_pos
				- (host_port_seperator_pos + 1);
	} else {
		portPartLength = url.length() - (host_port_seperator_pos + 1);
	}

	string portStr = url.substr(host_port_seperator_pos + 1, portPartLength);

	return atoi(portStr.c_str());

}

//put the data read from fd to HTTP server
void HttpHelper::putURIContent(HttpRequest request, HttpResponse & response, int fd)
{
	char targetPortStr [20];
	string targetHost;

	if (request.isUsingProxy()) {
		sprintf(targetPortStr, "%i", request.webProxyPort);
		targetHost = request.webProxyHost;
	} else {
		sprintf(targetPortStr, "%i", request.getPortInHostHeader());
		targetHost = request.getHostInHostHeader();
	}

	logger->debug("connecting to %s:%s", targetHost.c_str(), targetPortStr);


	//establish connection to http server:
	int clientSocket = tcp_connect(targetHost.c_str(), targetPortStr);
	if (-1 == clientSocket) {
		logger->error("failed to setup TCP connection");
		exit(-1);
	}

	// create the HTTP request string and push the file content
	string httpRequestStr = createHttpRequestStr(request);

	if (-1 == pushHttpMsg(httpRequestStr, clientSocket, fd)) {
		logger->error("failed to send HTTP message!");
		exit(-1);
	}

	char inputBuffer[1500];

	// now let's read the response
	int bufSize = read(clientSocket, inputBuffer, sizeof(inputBuffer));
	memset(_httpReponseBuffer, 0, sizeof(_httpReponseBuffer));
	uint32_t totalSize = 0;


	//read the response and try to find the separator between header and body - "\r\n\r\n" in the buffer
	while ((0 != bufSize) && (-1 != bufSize)) {

		// trying to find the separator between header and body - "\r\n\r\n" in the buffer
		bool foundEmptyLine = false;
		int contentStartPos = -1;
		const char * crlfcrlf = "\r\n\r\n";
		char * pch = strstr(inputBuffer, crlfcrlf);
		if (NULL != pch) {
			foundEmptyLine = true;
			contentStartPos = (pch - inputBuffer) + 4;
		}

		if (!foundEmptyLine) {
			// the whole buffer are httpResponse headers
			memcpy(_httpReponseBuffer + totalSize, inputBuffer, bufSize);
			totalSize += bufSize;

			//and continue to read
			memset(inputBuffer, 0, sizeof(inputBuffer));
			bufSize = read(clientSocket, inputBuffer, sizeof(inputBuffer));
			continue;

		} else {
			// copy only the bytes before contentStartPos
			memcpy(_httpReponseBuffer + totalSize, inputBuffer, contentStartPos);
			totalSize += contentStartPos;

			logger->debug("Response header has %d bytes", totalSize);

			logger->debug("HTTP response received:================");
			logger->debug(_httpReponseBuffer);

			int contentLength = getContentLength(string(_httpReponseBuffer));

			logger->debug("ContentLength is %d", contentLength);
			if (contentLength > 0) {
				logger->error("Strange. Why we receive content in a resonse to PUT?!!!");
			}


			// this is the end of looking at HTTP response stuff.
			break;

		}

	}

	close(clientSocket);

}

/*
 * sending out an HTTP request and return the HttpResponse
 * This function is not suitable for "streaming" type of http transactions where the response will "never" come to an end
 */
void HttpHelper::retrieveURIContent(HttpRequest request,
		HttpResponse & response) {


	char targetPortStr [20];
	string targetHost;

	if (request.isUsingProxy()) {
		sprintf(targetPortStr, "%i", request.webProxyPort);
		targetHost = request.webProxyHost;
	} else {
		sprintf(targetPortStr, "%i", request.getHostPortInUrl());
		targetHost = request.getHostNameInUrl();
	}

	logger->debug("connecting to %s:%s", targetHost.c_str(), targetPortStr);

	//establish connection to http server
	int clientSocket = tcp_connect(targetHost.c_str(), targetPortStr);

	if (-1 == clientSocket) {
		logger->error("failed to setup TCP connection");
		exit(-1);
	}

	// lets get the content ready
	string httpRequestStr = createHttpRequestStr(request);

	// now let's send out packet to remote server
	write(clientSocket, httpRequestStr.c_str(), httpRequestStr.length());
	logger->debug("sending out HTTP request:\n%s", httpRequestStr.c_str());

	unsigned int totalSize = 0;
	memset(_httpReponseBuffer, 0, sizeof(_httpReponseBuffer));

	// read from the socket, until we see two consecutive 0d0a0d0a
	// which is the separator between HTTP header and body
	// we then save the response to httpReponseBuffer
	bool httpContentStarted = false;

	//create the file to save http content
	DownloadFilefp = fopen(DOWNLOADED_FILE_NAME, "w");
	int32_t contentLength = -1;
	int32_t contentReadSoFar = 0;
	char buffer[1024];
	memset(buffer, 0, sizeof(buffer));
	int bufsize = read(clientSocket, buffer, sizeof(buffer));
	fprintf(stderr, ".");
	//read the response and try to find the separator between header and body - "\r\n\r\n" in the buffer
	while ((0 != bufsize) && (-1 != bufsize)) {

		// trying to find the separator between header and body - "\r\n\r\n" in the buffer
		bool foundEmptyLine = false;
		int contentStartPos = -1;
		const char * crlfcrlf = "\r\n\r\n";
		char * pch = strstr(buffer, crlfcrlf);
		if (NULL != pch) {
			foundEmptyLine = true;
			contentStartPos = (pch - buffer) + 4;
		}

		if (!foundEmptyLine) {
			// the whole buffer are httpResponse headers
			memcpy(_httpReponseBuffer + totalSize, buffer, bufsize);
			totalSize += bufsize;

			//and continue to read
			memset(buffer, 0, sizeof(buffer));
			bufsize = read(clientSocket, buffer, sizeof(buffer));
			fprintf(stderr, ".");

			continue;

		} else {
			// copy only the bytes before contentStartPos
			memcpy(_httpReponseBuffer + totalSize, buffer, contentStartPos);
			totalSize += contentStartPos;
			httpContentStarted = true;

			logger->debug("Response header has %d bytes", totalSize);

			logger->debug("HTTP response received:================");
			logger->debug(_httpReponseBuffer);

			contentLength = getContentLength(string(_httpReponseBuffer));

			logger->debug("ContentLength is %d", contentLength);

			//write the initial part of the content to the file
			contentReadSoFar = (bufsize - contentStartPos);
			fwrite(buffer + contentStartPos, 1,  contentReadSoFar, DownloadFilefp);
			totalSize += contentReadSoFar;

			// this is the end of looking at HTTP response stuff.
			break;

		}

	} // end of handling the initial part of the HTTP response stream

	if ((contentLength > 0) && (contentReadSoFar < contentLength)) {
		int bufsize = read(clientSocket, buffer, sizeof(buffer));
		fprintf(stderr, ".");

		//read the response and try to find the separator between header and body - "\r\n\r\n" in the buffer
		while ((0 != bufsize) && (-1 != bufsize) && (contentReadSoFar < contentLength)) {
			fwrite(buffer, 1,  bufsize, DownloadFilefp);

			totalSize += bufsize;

			contentReadSoFar += bufsize;

			if (contentReadSoFar == contentLength) {
				logger->debug("all content read!");
				break;
			}
			bufsize = read(clientSocket, buffer, sizeof(buffer));
			fprintf(stderr, ".");
		}

	}

	fprintf(stderr, "\n");

	close(clientSocket);

	fclose(DownloadFilefp);
	DownloadFilefp = NULL;

	logger->debug("totally read : %d bytes", totalSize);

}

string HttpHelper::createHttpRequestStr(HttpRequest httpRequest) {

//	GET /index HTTP/1.0
//	User-Agent: Wget/1.12 (linux-gnu)
//	Accept: */*
//	Host: nwprog1.netlab.hut.fi:3000
//	Connection: Keep-Alive

//	PUT /shaohongFileName HTTP/1.0
//	User-Agent: nwprogClient 1.0
//	Accept: */*
//	Host: nwprog1.netlab.hut.fi
//	Connection: close

	stringstream oss;

	oss << httpRequest.method << " " ;

	if (!httpRequest.isUsingProxy() ){
		// if we're not using proxy, just use the path as the Uri as I know the nwprog1.netlab.hut.fi:3000 server have problem with absolute-uri
		oss << httpRequest.getResourcePath() << " HTTP/1.1" << CRLF;
	} else {
		oss << httpRequest.requestUri << " HTTP/1.1" << CRLF;
	}


//	if (httpRequest.isUsingProxy()) {
//		// if webProxy is used, we need to put the whole http url in the request line
////		cerr << "client using proxy!" << endl;
//		oss << httpRequest.requestUri << " HTTP/1.1" << CRLF;
//	} else {
//		oss << httpRequest.getResourcePath() << " HTTP/1.1" << CRLF;
//	}

	//add the User-Agent header
	if (!httpRequest.userAgent.empty()) {
		oss << "User-Agent: "<< httpRequest.userAgent  << CRLF;
	}

	//add the Host header
	if (!httpRequest.hostHdrValue.empty()) {
		oss << "Host: "<< httpRequest.hostHdrValue  << CRLF;
	}

	//add the additional headers
	for (vector<string>::iterator itr = httpRequest.additionalHeaders.begin();
			itr < httpRequest.additionalHeaders.end(); itr++) {
		oss << *itr << CRLF;
	}
	oss << CRLF; // need an empty line at the end

	return oss.str();

}


int HttpHelper::getContentLength(string httpResponse) {
	const string tokenStr = "Content-Length";
	size_t tokenStartPos = httpResponse.find(tokenStr);

	if (string::npos == tokenStartPos) {
		return -1; // no such header found.
	}

	// skip the "Content-Length:" and read till we meet '\0d'
	int contentLengthValueStartPos = tokenStartPos + tokenStr.length() + 1;

	string contentLengthValueStr;
	int i=contentLengthValueStartPos;

	while(httpResponse.at(i)!='\x0d'){
		contentLengthValueStr.push_back(httpResponse.at(i));
		i++;
	}
	// now the contentLengthValue is saved in contentLengthValueStr

	return atoi(contentLengthValueStr.c_str());
}

HttpRequest * HttpHelper::parseHttpRequest(string receivedMsg)
{
	//	GET /index HTTP/1.0
	//	User-Agent: Wget/1.12 (linux-gnu)
	//	Accept: */*
	//	Host: nwprog1.netlab.hut.fi:3000
	//	Connection: Keep-Alive
	//

	stringstream ss(receivedMsg, ios_base::in);
	string currLine;

	// look at the first line of the message
	getline(ss, currLine);

	stringstream lineStream(currLine, ios_base::in);

	string method = getMethodFromRequestLine(currLine);

	HttpRequest * newRequest = new HttpRequest();
	newRequest->method = method;

	newRequest->requestUri = getRequestUriFromRequestLine(currLine);

	if (!isValidMethod(method)){
		logger ->error("invalid method : %s", method.c_str());
		throw HttpException("invalid method received");
	}

	//continue to get other headers until we see an empty line
	getline(ss, currLine);

	while(!Utils::isBlankLine(currLine)) {
//		cerr << "currLine is: " << currLine << endl;
		// check if this line is the host header
		string hostHdrToken = "Host: ";
		if ( 0 == currLine.find(hostHdrToken)) {
			newRequest->hostHdrValue = currLine.substr(hostHdrToken.length());

		} else {
			newRequest->additionalHeaders.push_back(string(currLine));
		}
		getline(ss, currLine);
	}
	return newRequest;
}

HttpResponse * HttpHelper::parseHttpResponse(string receivedMsg)
{
//HTTP/1.1 200 OK
//Date: Wed, 25 Apr 2012 13:55:54 GMT
//Server: Apache
//Last-Modified: Mon, 23 Apr 2012 04:36:31 GMT
//ETag: "6042b88-d9117-4be512eb0f844"
//Accept-Ranges: bytes
//Content-Length: 889111
//Connection: close
//Content-Type: image/jpeg

	stringstream ss(receivedMsg, ios_base::in);
	string currLine;

	// look at the first line of the message
	getline(ss, currLine);

	HttpResponse * newResponse = new HttpResponse();
	istringstream iss (currLine, istringstream::in);

	string httpVersion;
	iss >> httpVersion;
	iss >> newResponse->statusCode;
	getline(iss, newResponse->reasonPhrase);


	//continue to get other headers until we see an empty line
	getline(ss, currLine);

	while(!Utils::isBlankLine(currLine)) {
//		cerr << "currLine is: " << currLine << endl;
		// check if this line is the Content-Length header
		string contentLengthToken = "Content-Length: ";
		if ( 0 == currLine.find(contentLengthToken)) {
			string contentLengthStr = currLine.substr(contentLengthToken.length());
			newResponse->contentLength = atoi( contentLengthStr.c_str());

		} else {
			newResponse->additionalHeaders.push_back(string(currLine));
		}
		getline(ss, currLine);
	}
	return newResponse;

}


/**
 * here we assume the line passed in is a valid request line
 */
string HttpHelper::getMethodFromRequestLine(string& currLine) {
	//	request line is: "GET /index.html HTTP/1.0"
	size_t spacePos = currLine.find(' ');
	if (string::npos == spacePos) {
		throw HttpException("invalid request line " + currLine);
	}

	return currLine.substr(0, spacePos);

}

string HttpHelper::getRequestUriFromRequestLine(string& currLine) {
	//	request line is: "GET /index.html HTTP/1.0"
	size_t spacePos = currLine.find(' ');
	if (string::npos == spacePos) {
		throw HttpException("invalid request line " + currLine);
	}

	size_t pathStartPos = spacePos + 1;

	size_t pathEndPos = currLine.find(' ', pathStartPos);
	return currLine.substr(pathStartPos,
			pathEndPos - pathStartPos);
}

bool HttpHelper::isValidMethod(string& methodName) {
	if (0 == methodName.compare("GET")) {
		return true;
	}

	if (0 == methodName.compare("PUT")) {
		return true;
	}

	return false;
}

// cancel the on-going HTTP processing
void HttpHelper::cancelProcessing() {

	logger->info("HTTP processing is interrupted!");

	// close the downloaded file fp
	if (NULL != DownloadFilefp) {
		fclose(DownloadFilefp);
		DownloadFilefp = NULL;

		// remove the file that has partially downloaded file
		unlink(DOWNLOADED_FILE_NAME);

		logger->info("downloading was interrupted. \"%s\" is removed", DOWNLOADED_FILE_NAME);
	}
}

/**
 * push the http message through the clientSocket. the content of the http message is read from fd
  */
int HttpHelper::pushHttpMsg(string httpMsgHeaders, int clientSocket, int fd) {
	//Here we use a big output buffer so that packet size can match MTU
	//when many packets are needed to upload the content.
	//This helps to improve the efficiency of utilizing the transport and link layer.
	char outputBuffer[10000];
	uint32_t outputBufferDataSize = 0;	// the actual data size in output buffer

	// now let's send out packet to remote server
//	write(clientSocket, httpRequestStr.c_str(), httpRequestStr.length());
	logger->debug("sending out HTTP message:\n%s", httpMsgHeaders.c_str());

	// write the HTTP headers to output buffer
	memcpy(outputBuffer, httpMsgHeaders.c_str(), httpMsgHeaders.length());
	outputBufferDataSize +=  httpMsgHeaders.length();

	if (fd > 0) {
		// keep on reading and writing the content to the connection until the eof is met
		char inputBuffer[1400];
		int bufSize = read(fd, inputBuffer, sizeof(inputBuffer));

		while ((0 != bufSize) && (-1 != bufSize)) {

			if ((bufSize + outputBufferDataSize) > sizeof(outputBuffer)) {
				// flush output buffer
				Utils::flushOutputBuffer(clientSocket, outputBuffer,
						outputBufferDataSize);
				fprintf(stderr, ".");

//			write(clientSocket, outputBuffer, outputBufferDataSize);
				outputBufferDataSize = 0;
			}

			//save the content to write into outputbuffer
			memcpy(outputBuffer + outputBufferDataSize, inputBuffer, bufSize);
			outputBufferDataSize += bufSize;

			bufSize = read(fd, inputBuffer, sizeof(inputBuffer));
		}
	}

	Utils::flushOutputBuffer(clientSocket, outputBuffer, outputBufferDataSize);

	fprintf(stderr, "\n");
	logger->debug("finished writing content");

	return 0;
}

//checks whether the given buffer ends with the CRLFCRLF separator
// between HTTP header and HTTP content
bool HttpHelper::endsWithSeparator(char * buffer) {

	int bufLen = strlen(buffer);

	if (bufLen < 4) {
		return false;
	}

	if ((buffer[bufLen - 4] == '\r') && (buffer[bufLen - 3] == '\n')
			&& (buffer[bufLen - 2] == '\r') && (buffer[bufLen - 1] == '\n')) {
		return true;
	}

	return false;
}


/**
 * from the connfd, read the http headers and save data to buffer.
 * the data to be read will be less than the buffSize
 */
int HttpHelper::readHttpHeaders(int connfd, char * buffer, int buffSize) {

	int headerBytesRead = 0;
	while (headerBytesRead < buffSize) {
		int readRet = read(connfd, buffer + headerBytesRead, 1);
		if (-1 == readRet) { // meet errors in reading
			logger->error("meet error in reading from network connection");
			perror("error met reading from network");
			return -1;
		}

		if (0 == readRet) { // EOF is met
			break;
		}

		headerBytesRead++;
		if (HttpHelper::endsWithSeparator(buffer)) {
			return headerBytesRead;
		}
	}

	return headerBytesRead;
}


HttpResponse * HttpHelper::readHttpResponse(int connfd) {
	char buffer[1500];
	memset(buffer, 0, sizeof(buffer));

	// Read the initial part of the HTTP response into buffer
	int headerBytes = readHttpHeaders(connfd, buffer,sizeof(buffer));
	if (headerBytes == -1) {
		return NULL;
	}
//	logger->debug("received the following response:\n%s ", buffer);

	return HttpHelper::parseHttpResponse(string(buffer));
}


bool HttpHelper::isDirectoryListing(HttpRequest & request)
{
    if (0 == request.getResourcePath().compare("/")) {
    	return true;
    }

    if (0 == request.getResourcePath().compare("/index")) {
    	return true;
    }

    return false;
}
